import { analytics } from "@/wab/client/observability";
import {
  AssertionError,
  FalsyValueError,
  HarmlessError,
  NullOrUndefinedValueError,
  shallowJson,
  stampObjectUuid,
} from "@/wab/shared/common";
import { DEVFLAGS } from "@/wab/shared/devflags";
import { isStampedIgnoreError } from "@/wab/shared/error-handling";
import { UserError } from "@/wab/shared/UserError";
import * as Sentry from "@sentry/browser";
import { notification } from "antd";
import { IconType } from "antd/lib/notification";

const errorMessageCounters = new Map<string, number>();

function mungeErrorMsg(msg: string) {
  return DEVFLAGS.mungeErrorMessages[msg] ?? msg;
}

// Note that sometimes you will see double errors, due to React rethrowing
// errors and/or mobx rethrowing errors.  I don't fully understand the mechanics
// (there is some magic that React does across execution contexts) but this only
// happens in development and not production.
export function showError(
  error: Error,
  opts?: {
    title?: string;
    description?: string;
    type?: IconType;
  }
) {
  const { title = "Unexpected error", description, type } = opts ?? {};
  const deriveErrorInfo = () => {
    if (error.message?.includes("XHRStatus0Error")) {
      return {
        title: "Temporarily disconnected",
        description:
          "Your browser is disconnected from Plasmic. Saving is paused, and will resume soon!",
        type: type ?? ("warning" as const),
      };
    } else {
      return {
        title: error instanceof UserError ? error.message : title,
        description:
          error instanceof UserError ? error.description : `${error}`,
        type: type ?? ("error" as const),
      };
    }
  };

  const info = deriveErrorInfo();
  const newCount = 1 + (errorMessageCounters.get(info.title) || 0);
  errorMessageCounters.set(title, newCount);
  const occurrences = newCount > 1 ? ` (${newCount} occurrences)` : "";

  notification.open({
    type: info.type,
    key: info.title,
    message: info.title + occurrences,
    description: description ?? mungeErrorMsg(info.description),
    duration: info.type === "warning" ? 10 : 0,
    onClose: () => {
      errorMessageCounters.delete(title);
    },
  });
}

/**
 * This is intended to be called from onerror and onunhandledrejection, so it
 * breaks out the processing into separate ticks (so that any errors from
 * *those* will in turn be sure to raise more errors loudly).  We just want to
 * be careful never to silence things.
 */
export function handleError(error: Error, source?: string) {
  if (shouldIgnoreError(error, source)) {
    return;
  }
  setTimeout(() => {
    if (DEVFLAGS.debug || shouldShowError(error)) {
      showError(error);
    }
  }, 0);
  setTimeout(() => {
    stampObjectUuid(error);
    Sentry.captureException(error);
    analytics().track("Error", {
      error: shallowJson(error),
    });
  }, 0);
}

function shouldShowError(error: Error) {
  if (
    error instanceof AssertionError ||
    error instanceof NullOrUndefinedValueError ||
    error instanceof FalsyValueError
  ) {
    if ((window as any).isProd) {
      // For now, we don't show AssertionErrors, NullOrUndefinedValueErrors, FalsyValueError to users
      return false;
    }
  }
  return true;
}

// The following is a list of "harmless" errors thrown by third-party code, the list is used both
// to ignore errors that are handled and should not be shown to the user, and to ignore errors that
// are unhandled and should not be reported
export const ERROR_PATTERNS_TO_IGNORE = [
  // react-aria
  // https://app.shortcut.com/plasmic/story/38632/typeerror-failed-to-execute-createtreewalker-on-document-parameter-1-is-not-of-type-node
  "createTreeWalker",

  // monaco
  // https://app.shortcut.com/plasmic/story/39095/error-cannot-read-properties-of-null-reading-getstartposition
  "reading 'getStartPosition'",
  // Monaco tried to use web workers, but the apphost can block it with CSP
  ".loadForeignModule",

  // antd / rc-align sometimes throw this by using ReactDOM.findDOMNode()
  "Unable to find node on an unmounted component",

  // This is to avoid an error message generated by the Antd Button onClick()
  // (that doesn't work well cross frame) of the Antd Tabs Custom Action
  "Failed to execute 'appendChild'",

  // TinyMCE editor throws this error when embedding media
  // with iframe snippet, and opening the media modal again;
  // it tries to look for a "data" field, which doesn't exist
  // from the data object parsed from the iframe snippet.
  "Could not find valid *required* value for",

  // This is to avoid an error message generated by React Joy Ride when the target
  // element goes away during the tour step
  "isSameNode",

  // This is a known benign error that happens with ResizeObserver
  // https://linear.app/plasmic/issue/PLA-1721
  "ResizeObserver loop completed with undelivered notifications.",
];

export function shouldIgnoreError(error: Error, source?: string) {
  if (
    error.message &&
    ERROR_PATTERNS_TO_IGNORE.some((pattern) => error.message.includes(pattern))
  ) {
    return true;
  }

  if (error instanceof HarmlessError) {
    return true;
  }

  // Monaco editor worker sometimes throws and the error gets to `window.error`
  if (
    error.message?.toLowerCase().includes("debug failure") &&
    source?.toLowerCase().includes("monaco")
  ) {
    return true;
  }

  if (isStampedIgnoreError(error)) {
    return true;
  }

  return false;
}

export function reportError(error: Error, eventName?: string) {
  if (error instanceof UserError) {
    if (DEVFLAGS.debug) {
      console.log("UserError: ", eventName ?? error.name, error.message, error);
    }
    return;
  }
  console.log("Error: ", eventName ?? error.name, error.message, error);
  Sentry.captureException(error);
  analytics().track(eventName ?? error.name, {
    name: error.name,
    message: error.message,
  });
}

export function reportSilentErrorMessage(
  msg: string,
  eventName = "Silent Error"
) {
  Sentry.captureMessage(msg);
  analytics().track(eventName, {
    message: msg,
  });
}

// Sometimes the handled variable is not of type Error, so we need to normalize it
// https://linear.app/plasmic/issue/PLA-11224/captureexception-error
export function normalizeError(error: any) {
  return isStampedIgnoreError(error)
    ? error
    : error instanceof Error
    ? error
    : error && error.error
    ? (error.error as Error)
    : typeof error === "string"
    ? new Error(error)
    : new Error(
        `Unknown error: ${
          typeof error === "object" ? JSON.stringify(error) : error
        }`
      );
}
